package org.runecore.rs2.model.player;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

import org.runecore.Launch;
import org.runecore.rs2.model.World;
import org.runecore.rs2.net.ISAACCipher;
import org.runecore.rs2.net.StreamBuffer;
import org.runecore.rs2.net.codec.AbstractProtocolDecoder;

/**
 * RuneCore Sep 4, 2011
 * 
 * @author Harry Andreas<Harry@RuneCore.org>
 */
public class GameSession {

	/**
	 * The <code>SelectionKey</code> for identifing packets
	 */
	private SelectionKey key;

	/**
	 * The <code>Player</code> for this session
	 */
	private Player player;

	/**
	 * The <code>ConnectionStage</code> of the connection
	 */
	private ConnectionStage stage;

	/**
	 * The <code>ByteBuffer</code> for reading packets
	 */
	private ByteBuffer inBuffer;

	/**
	 * The <code>SocketChannel</code> for the <code>GameSession</code>
	 */
	private SocketChannel channel;
	
	/**
	 * The <code>AbstractProtocolDecoder</code> for decoding
	 */
	private AbstractProtocolDecoder decoder;
	
	/**
	 * The <code>ISAACCipher</code> for Decrypting packets
	 */
	private ISAACCipher decryptor;

	/**
	 * The <code>ISAACCipher</code> for encrypting packets
	 */
	private ISAACCipher encryptor;
	
	/**
	 * The packet opcode
	 */
	private int packetOpcode = -1;
	
	/**
	 * The packet length
	 */
	private int packetLength = -1;
	
	/**
	 * The packet length's for the game version
	 */
	private final int[] packetLengths = World.getSingleton().getSelectedRevision().getPacketSizes();

	/**
	 * Constructor
	 * @param key
	 */
	public GameSession(SelectionKey key) {
		setKey(key);
		setStage(ConnectionStage.CONNECTED);
		setInBuffer(ByteBuffer.allocateDirect(512));
		setChannel((SocketChannel) key.channel());
		setDecoder(World.getSingleton().getSelectedRevision().getProtocolDecoder());
	}

	/**
	 * Handles InStream data
	 */
	public void handleInstream() {
		try {
			if(!getKey().isValid()) {
				return;
			}			
			if (getChannel().read(getInBuffer()) == -1) {
				exit();
				return;
			}
			getInBuffer().flip();
			while (getInBuffer().hasRemaining()) {
				if(getStage() != ConnectionStage.LOGGED_IN) {
					getDecoder().decodeProtocol(this);
					break;
				}
				//decode packet here!
				if(packetOpcode == -1) {
					packetOpcode = getInBuffer().get() & 0xff;
					packetOpcode = packetOpcode - getDecryptor().getNextValue() & 0xff;
				}
				
				//determine packet length
				if(packetLength == -1) {
					packetLength = getPacketLengths()[packetOpcode];
					if(packetLength == -1) {
						if (!getInBuffer().hasRemaining()) {
							getInBuffer().flip();
							getInBuffer().compact();
							break;
						}
						packetLength = getInBuffer().get() & 0xff;
					}
				}
				//finally handle the packet
				if (getInBuffer().remaining() >= packetLength) {
					handlePackets();
					resetPackets();
				} else {
					getInBuffer().flip();
					getInBuffer().compact();
					break;
				}
			}
			getInBuffer().clear();
		} catch (Exception e) {
			exit();
		}
	}
	
	/**
	 * Handle's the inbound packets
	 */
	public void handlePackets() {
		int positionBefore = getInBuffer().position();
		StreamBuffer.InBuffer in = StreamBuffer.newInBuffer(getInBuffer());
		try {
			World.getSingleton().getSelectedRevision().getPacketDecoder().decodePacket(this, packetOpcode, in);
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			int read = getInBuffer().position() - positionBefore;
			for (int i = read; i < packetLength; i++) {
				getInBuffer().get();
			}
		}
	}
	
	/**
	 * Reset's the packet lengths
	 */
	public void resetPackets() {
		packetOpcode = -1;
		packetLength = -1;
	}


	/**
	 * Closes the connection
	 */
	public void exit() {
		Launch.getEnv().getLogger().
		info("Session "+ getChannel().socket().getRemoteSocketAddress()+ " has disconnected.");
		try {
			if(getPlayer() != null) {
				World.getSingleton().getWorldPlayers().remove(getPlayer());
			}
			getChannel().close();
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			World.getSingleton().getSessionMap().remove(getKey());
			key.cancel();
		}
	}

	/**
	 * Send's a packet
	 * @param buffer
	 */
	public void sendPacket(ByteBuffer buffer) {
		buffer.flip();
		try {
			if(!getKey().isValid()) {
				return;
			}	
			getChannel().write(buffer);
		} catch (IOException e) {
			e.printStackTrace();
			exit();
		}
	}

	/**
	 * RuneCore Sep 4, 2011
	 * 
	 * @author Harry Andreas<Harry@RuneCore.org>
	 */
	public enum ConnectionStage {
		CONNECTED, UPDATE_SERVER, UPDATE_SERVER_2, HASH_CHECK, LOGGING_IN, LOGGED_IN;
	}

	/**
	 * @param key
	 *            the key to set
	 */
	public void setKey(SelectionKey key) {
		this.key = key;
	}

	/**
	 * @return the key
	 */
	public SelectionKey getKey() {
		return key;
	}

	/**
	 * @param player
	 *            the player to set
	 */
	public void setPlayer(Player player) {
		this.player = player;
	}

	/**
	 * @return the player
	 */
	public Player getPlayer() {
		return player;
	}

	/**
	 * @param stage
	 *            the stage to set
	 */
	public void setStage(ConnectionStage stage) {
		this.stage = stage;
	}

	/**
	 * @return the stage
	 */
	public ConnectionStage getStage() {
		return stage;
	}

	/**
	 * @param inBuffer
	 *            the inBuffer to set
	 */
	public void setInBuffer(ByteBuffer inBuffer) {
		this.inBuffer = inBuffer;
	}

	/**
	 * @return the inBuffer
	 */
	public ByteBuffer getInBuffer() {
		return inBuffer;
	}

	/**
	 * @param channel
	 *            the channel to set
	 */
	public void setChannel(SocketChannel channel) {
		this.channel = channel;
	}

	/**
	 * @return the channel
	 */
	public SocketChannel getChannel() {
		return channel;
	}

	/**
	 * @param decoder the decoder to set
	 */
	public void setDecoder(AbstractProtocolDecoder decoder) {
		this.decoder = decoder;
	}

	/**
	 * @return the decoder
	 */
	public AbstractProtocolDecoder getDecoder() {
		return decoder;
	}

	/**
	 * @param decryptor the decryptor to set
	 */
	public void setDecryptor(ISAACCipher decryptor) {
		this.decryptor = decryptor;
	}

	/**
	 * @return the decryptor
	 */
	public ISAACCipher getDecryptor() {
		return decryptor;
	}

	/**
	 * @param encryptor the encryptor to set
	 */
	public void setEncryptor(ISAACCipher encryptor) {
		this.encryptor = encryptor;
	}

	/**
	 * @return the encryptor
	 */
	public ISAACCipher getEncryptor() {
		return encryptor;
	}

	/**
	 * @param packetOpcode the packetOpcode to set
	 */
	public void setPacketOpcode(int packetOpcode) {
		this.packetOpcode = packetOpcode;
	}

	/**
	 * @return the packetOpcode
	 */
	public int getPacketOpcode() {
		return packetOpcode;
	}

	/**
	 * @return the packetLengths
	 */
	public int[] getPacketLengths() {
		return packetLengths;
	}

}